M2.991 · Aprenentatge automàtic · PAC1
2024-1 · Màster universitari en Ciència de dades (Data science)
Estudis d'Informàtica, Multimèdia i Telecomunicació
L'objectiu principal d'aquesta primera PAC és que us familiaritzeu amb l'entorn de treball que utilitzareu en la resta de pràctiques de l'assignatura. Aquest entorn estarà format per un conjunt de dependències relatives a certs mòduls de Python que seran necessaris per poder executar la vostra PAC de manera correcta. Aquestes dependències les gestionarem gràcies a l'ajuda d'Anaconda. Una altra de les eines fonamentals del que serà el vostre nou entorn de treball serà Jupyter, que us permetrà treballar amb Notebooks (fitxers *.ipynb) com el present enunciat.
Un altre dels aspectes més importants que cobrirem en aquesta primera PAC, tal com indica el títol, és el de la preparació de les dades. En aquesta PAC aprendrem a carregar un conjunt de dades o dataset i ens ajudarem d'eines de visualització per comprendre millor com es distribueixen les dades amb l'objectiu d'entendre com podem treure'n profit. A més, ens acostumarem a treballar amb conjunts d'entrenament i de prova per confirmar si les conclusions que traiem sobre una part de les mostres es poden generalitzar i extrapolar a la resta.
En resum, en aquesta pràctica veurem com aplicar diferents tècniques per a la càrrega i preparació de dades seguint els passos llistats a continuació:
- Càrrega d'un conjunt de dades (1 punt)
- Anàlisi de les dades (2.5 punts)
2.1. Anàlisi estadístic bàsic
2.2. Anàlisi exploratori de les dades - Preprocessament de les dades (1.5 punts)
- Reducció de la dimensionalitat (2.5 punts)
- Conjunts desbalancejats de dades (2.5 punts)
5.1. Oversampling
Important: cada un dels exercicis pot suposar diversos minuts d'execució, per la qual cosa l'entrega s'ha de fer en format notebook i en format html, on es vegi el codi, els resultats i comentaris de cada exercici. Es pot exportar el notebook a html des del menú File $\to$ Download as $\to$ HTML.
Important: existeix un tipus de cel·la especial per a albergar text. Aquest tipus de cel·la us serà molt útil per respondre a les diferents preguntes teòriques plantejades al llarg de cada PAC. Per canviar el tipus de cel·la a aquest tipus, escolliu en el menú: Cell $\to$ Cell Type $\to$ Markdown.
Important: la solució plantejada no ha d'utilitzar mètodes, funcions o paràmetres declarats "deprecated" en futures versions.
Important: no oblideu posar el vostre nom i cognoms a la següent cel·la.
Toni Vives Cabaleiro
Per la realització de la pràctica, necessitarem importar els següents mòduls:
import numpy as np
import pandas as pd
from sklearn import preprocessing
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
import seaborn as sns
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
import matplotlib
import matplotlib.pyplot as plt
pd.set_option('display.max_columns', None)
seed = 100
%matplotlib inline
Càrrega del conjunt de dades (1 punt)¶
Al llarg de tota la PAC treballarem amb el conjunt de dades anomenat Bank Marketing, que és un dels datasets disponibles al Repositori d'Aprenentatge Automàtic de la Universitat de Califòrnia a Irvine.
A l'enllaç [https://archive.ics.uci.edu/dataset/222/bank+marketing] teniu disponible tant el conjunt de dades Bank Marketing esmentat com tota la informació rellevant necessària per comprendre millor amb quin tipus de dades treballarem. En resum, les dades d'aquest dataset estan relacionades amb campanyes de màrqueting directe (trucades telefòniques) d'una institució bancària portuguesa. L'objectiu que es busca en aquest conjunt de dades és predir si el client contractarà un dipòsit a termini o no (variable y).
En primer lloc, haureu de carregar al Notebook el conjunt de dades amb el qual treballarem durant la resta de la PAC. Per fer-ho, podeu descarregar-lo manualment des de l'enllaç referit prèviament, tot i que us aconsellem que instal·leu i utilitzeu el mòdul ucimlrepo tal com s'explica a la pàgina del dataset.
- El nombre i els noms dels atributs descriptius (variables que podrien ser utilitzades per predir la variable objectiu "y").
- El nombre de files (mostres) del conjunt de dades.
- Verifiqueu si hi ha "missing values", i si és així, en quines columnes.
Suggeriment: si utilitzeu ucimlrepo, exploreu els atributs metadata i variables de l'objecte obtingut.
Suggeriment: separeu el conjunt de dades original en les variables "X" (atributs descriptius) i "y" (variable objectiu), tot i que potser us sigui útil en algun moment tenir-les també en un únic DataFrame combinat.
Començarem analitzant la informació que ens mostra el propi dataset en el repositori d'Aprenentatge Automàtic de la Universitat de Califòrnia a Irvine:
Taula de variables
| Nom de la variable | Rol | Tipus | Demogràfic | Descripció |
|---|---|---|---|---|
| edat | Característica | Enter | Edat | |
| feina | Característica | Categòric | Ocupació | Tipus de feina (categòric: 'administrador', 'obrer', 'empresari', 'empleat domèstic', 'gerent', 'jubilat', 'autònom', 'serveis', 'estudiant', 'tècnic', 'aturat', 'desconegut') |
| estat civil | Característica | Categòric | Estat civil | Estat civil (categòric: 'divorciat', 'casat', 'solter', 'desconegut'; nota: 'divorciat' significa divorciat o vidu) |
| educació | Característica | Categòric | Nivell d'educació | (categòric: 'bàsic.4a', 'bàsic.6a', 'bàsic.9a', 'batxillerat', 'analfabet', 'curs professional', 'títol universitari', 'desconegut') |
| per defecte | Característica | Binari | Té crèdit en mora? | |
| saldo | Característica | Enter | Saldo mitjà anual | |
| habitatge | Característica | Binari | Té préstec hipotecari? | |
| préstec | Característica | Binari | Té préstec personal? | |
| contacte | Característica | Categòric | Tipus de comunicació de contacte (categòric: 'mòbil', 'telèfon') | |
| dia_de_la_setmana | Característica | Data | Últim dia de contacte de la setmana | |
| mes | Característica | Data | Últim mes de contacte de l'any (categòric: 'gen', 'feb', 'mar', ..., 'nov', 'des') | |
| durada | Característica | Enter | Durada de l'últim contacte, en segons (numèric). Nota: Aquest atribut afecta considerablement el resultat final. Si la durada és 0, el resultat és "no". Aquesta entrada s'ha d'incloure només amb finalitats de referència i s'ha de descartar si es vol un model predictiu realista. | |
| campanya | Característica | Enter | Nombre de contactes realitzats durant aquesta campanya i per aquest client (numèric, inclou l'últim contacte) | |
| dies de pau | Característica | Enter | Nombre de dies des que es va contactar per última vegada al client d'una campanya anterior (numèric; -1 significa que el client no va ser contactat prèviament) | |
| anterior | Característica | Enter | Nombre de contactes realitzats abans d'aquesta campanya i per aquest client. | |
| resultat | Característica | Categòric | Resultat de la campanya de màrqueting anterior (categòric: 'fracàs', 'inexistent', 'èxit') | |
| y | Objectiu | Binari | El client ha subscrit un dipòsit a termini? |
from ucimlrepo import fetch_ucirepo
# fetch dataset
bank_marketing = fetch_ucirepo(id=222)
# data (as pandas dataframes)
X = bank_marketing.data.features
y = bank_marketing.data.targets
df = X
df["y"] = y
# mostrem la metadata
bank_marketing.metadata
{'uci_id': 222,
'name': 'Bank Marketing',
'repository_url': 'https://archive.ics.uci.edu/dataset/222/bank+marketing',
'data_url': 'https://archive.ics.uci.edu/static/public/222/data.csv',
'abstract': 'The data is related with direct marketing campaigns (phone calls) of a Portuguese banking institution. The classification goal is to predict if the client will subscribe a term deposit (variable y).',
'area': 'Business',
'tasks': ['Classification'],
'characteristics': ['Multivariate'],
'num_instances': 45211,
'num_features': 16,
'feature_types': ['Categorical', 'Integer'],
'demographics': ['Age', 'Occupation', 'Marital Status', 'Education Level'],
'target_col': ['y'],
'index_col': None,
'has_missing_values': 'yes',
'missing_values_symbol': 'NaN',
'year_of_dataset_creation': 2014,
'last_updated': 'Fri Aug 18 2023',
'dataset_doi': '10.24432/C5K306',
'creators': ['S. Moro', 'P. Rita', 'P. Cortez'],
'intro_paper': {'ID': 277,
'type': 'NATIVE',
'title': 'A data-driven approach to predict the success of bank telemarketing',
'authors': 'Sérgio Moro, P. Cortez, P. Rita',
'venue': 'Decision Support Systems',
'year': 2014,
'journal': None,
'DOI': '10.1016/j.dss.2014.03.001',
'URL': 'https://www.semanticscholar.org/paper/cab86052882d126d43f72108c6cb41b295cc8a9e',
'sha': None,
'corpus': None,
'arxiv': None,
'mag': None,
'acl': None,
'pmid': None,
'pmcid': None},
'additional_info': {'summary': "The data is related with direct marketing campaigns of a Portuguese banking institution. The marketing campaigns were based on phone calls. Often, more than one contact to the same client was required, in order to access if the product (bank term deposit) would be ('yes') or not ('no') subscribed. \n\nThere are four datasets: \n1) bank-additional-full.csv with all examples (41188) and 20 inputs, ordered by date (from May 2008 to November 2010), very close to the data analyzed in [Moro et al., 2014]\n2) bank-additional.csv with 10% of the examples (4119), randomly selected from 1), and 20 inputs.\n3) bank-full.csv with all examples and 17 inputs, ordered by date (older version of this dataset with less inputs). \n4) bank.csv with 10% of the examples and 17 inputs, randomly selected from 3 (older version of this dataset with less inputs). \nThe smallest datasets are provided to test more computationally demanding machine learning algorithms (e.g., SVM). \n\nThe classification goal is to predict if the client will subscribe (yes/no) a term deposit (variable y).",
'purpose': None,
'funded_by': None,
'instances_represent': None,
'recommended_data_splits': None,
'sensitive_data': None,
'preprocessing_description': None,
'variable_info': 'Input variables:\n # bank client data:\n 1 - age (numeric)\n 2 - job : type of job (categorical: "admin.","unknown","unemployed","management","housemaid","entrepreneur","student",\n "blue-collar","self-employed","retired","technician","services") \n 3 - marital : marital status (categorical: "married","divorced","single"; note: "divorced" means divorced or widowed)\n 4 - education (categorical: "unknown","secondary","primary","tertiary")\n 5 - default: has credit in default? (binary: "yes","no")\n 6 - balance: average yearly balance, in euros (numeric) \n 7 - housing: has housing loan? (binary: "yes","no")\n 8 - loan: has personal loan? (binary: "yes","no")\n # related with the last contact of the current campaign:\n 9 - contact: contact communication type (categorical: "unknown","telephone","cellular") \n 10 - day: last contact day of the month (numeric)\n 11 - month: last contact month of year (categorical: "jan", "feb", "mar", ..., "nov", "dec")\n 12 - duration: last contact duration, in seconds (numeric)\n # other attributes:\n 13 - campaign: number of contacts performed during this campaign and for this client (numeric, includes last contact)\n 14 - pdays: number of days that passed by after the client was last contacted from a previous campaign (numeric, -1 means client was not previously contacted)\n 15 - previous: number of contacts performed before this campaign and for this client (numeric)\n 16 - poutcome: outcome of the previous marketing campaign (categorical: "unknown","other","failure","success")\n\n Output variable (desired target):\n 17 - y - has the client subscribed a term deposit? (binary: "yes","no")\n',
'citation': None}}
df.head(10)
| age | job | marital | education | default | balance | housing | loan | contact | day_of_week | month | duration | campaign | pdays | previous | poutcome | y | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 58 | management | married | tertiary | no | 2143 | yes | no | NaN | 5 | may | 261 | 1 | -1 | 0 | NaN | no |
| 1 | 44 | technician | single | secondary | no | 29 | yes | no | NaN | 5 | may | 151 | 1 | -1 | 0 | NaN | no |
| 2 | 33 | entrepreneur | married | secondary | no | 2 | yes | yes | NaN | 5 | may | 76 | 1 | -1 | 0 | NaN | no |
| 3 | 47 | blue-collar | married | NaN | no | 1506 | yes | no | NaN | 5 | may | 92 | 1 | -1 | 0 | NaN | no |
| 4 | 33 | NaN | single | NaN | no | 1 | no | no | NaN | 5 | may | 198 | 1 | -1 | 0 | NaN | no |
| 5 | 35 | management | married | tertiary | no | 231 | yes | no | NaN | 5 | may | 139 | 1 | -1 | 0 | NaN | no |
| 6 | 28 | management | single | tertiary | no | 447 | yes | yes | NaN | 5 | may | 217 | 1 | -1 | 0 | NaN | no |
| 7 | 42 | entrepreneur | divorced | tertiary | yes | 2 | yes | no | NaN | 5 | may | 380 | 1 | -1 | 0 | NaN | no |
| 8 | 58 | retired | married | primary | no | 121 | yes | no | NaN | 5 | may | 50 | 1 | -1 | 0 | NaN | no |
| 9 | 43 | technician | single | secondary | no | 593 | yes | no | NaN | 5 | may | 55 | 1 | -1 | 0 | NaN | no |
Hem fet la importació de les dades de la manera que s'ha especificat, mitjançant la llibreria ucimlrepo. d'aquí hem obtingut X i y, no obstant això, els hem unit per tal de poder realitzar posteriorment el train i test corresponent separant com nosaltres vulguem el dataset.
A continuació mostrarem una descripció de les dades i de la estructura que tenen.
print("size = ", df.shape)
df.describe()
size = (45211, 17)
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| count | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 |
| mean | 40.936210 | 1362.272058 | 15.806419 | 258.163080 | 2.763841 | 40.197828 | 0.580323 |
| std | 10.618762 | 3044.765829 | 8.322476 | 257.527812 | 3.098021 | 100.128746 | 2.303441 |
| min | 18.000000 | -8019.000000 | 1.000000 | 0.000000 | 1.000000 | -1.000000 | 0.000000 |
| 25% | 33.000000 | 72.000000 | 8.000000 | 103.000000 | 1.000000 | -1.000000 | 0.000000 |
| 50% | 39.000000 | 448.000000 | 16.000000 | 180.000000 | 2.000000 | -1.000000 | 0.000000 |
| 75% | 48.000000 | 1428.000000 | 21.000000 | 319.000000 | 3.000000 | -1.000000 | 0.000000 |
| max | 95.000000 | 102127.000000 | 31.000000 | 4918.000000 | 63.000000 | 871.000000 | 275.000000 |
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 45211 entries, 0 to 45210 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 45211 non-null int64 1 job 44923 non-null object 2 marital 45211 non-null object 3 education 43354 non-null object 4 default 45211 non-null object 5 balance 45211 non-null int64 6 housing 45211 non-null object 7 loan 45211 non-null object 8 contact 32191 non-null object 9 day_of_week 45211 non-null int64 10 month 45211 non-null object 11 duration 45211 non-null int64 12 campaign 45211 non-null int64 13 pdays 45211 non-null int64 14 previous 45211 non-null int64 15 poutcome 8252 non-null object 16 y 45211 non-null object dtypes: int64(7), object(10) memory usage: 5.9+ MB
amb aquesta descripció de les dades numèriques, podem veure que hi ha una gran diversitat entre els valors de les dades, fet que per poder-los comparar, possiblement caldrà aplicar una normalització de les dades.
Podem observar com a l'imprimir el shape, el dataFrame te 45211 files per 17 columnes, però al extreure la info() del dataFrame, veiem que no totes les columnes tenen la mateixa quantitat de dades, fet que fa pensar que hi haurà dades "null". Ho mostrem a continuació:
# confirmem que no hi ha dades faltants
df.isna().sum()
age 0 job 288 marital 0 education 1857 default 0 balance 0 housing 0 loan 0 contact 13020 day_of_week 0 month 0 duration 0 campaign 0 pdays 0 previous 0 poutcome 36959 y 0 dtype: int64
colNa = []
for column in df.columns:
if df[column].isna().sum() > 0:
colNa.append([column,int(df[column].isna().sum())])
print(" la llista de columnes que contenen valors nuls es = ", colNa)
la llista de columnes que contenen valors nuls es = [['job', 288], ['education', 1857], ['contact', 13020], ['poutcome', 36959]]
Tal com diu el repositori:
"Les dades estan relacionades amb campanyes de màrqueting directe (trucades telefòniques) d'una institució bancària portuguesa. L'objectiu de la classificació és predir si el client subscriurà un dipòsit a termini (variable y)."
Al tenir la variable y com a etiqueta, podem definir el problema com a un problema d'aprenentatge automàtic supervisat. i tal com diu l'enunciat del repositori, l'objectiu és classificar si el client subscriurà o no un dipòsit a termini, per tant es una classificació d'una variable binària.
Es tracta d'un problema d'aprenentatge automàtic supervisat de classificació.
Anàlisi de les dades (2.5 punts)¶
En aquest apartat visualitzarem cadascuna de les columnes o features del conjunt de dades per comprendre millor quina distribució tenen.
Anàlisi estadística bàsica¶
- Variables categòriques:
- Calculeu la freqüència.
- Feu un gràfic de barres per cada variable.
- Variables numèriques:
- Calculeu estadístics descriptius bàsics: mitjana, mediana, desviació estàndard, ...
- Feu un histograma per a cada variable.
Variables categòriques¶
A continuació començarem mostrant els gràfics de les variables categòriques.
Com hem vist anteriorment, els tipus de dades que tenim son o bé del format "object", o del format "int64". Ens aprofitarem d'aquesta informació per tal de separar els arrays de categories i els de valors numèrics.
#seleccionem les variables categoriques del dataframe
categorical_columns = df.select_dtypes(include=['object']).columns
A continuació recorrerem tot l'array i per cada variable categòrica, imprimirem el recompte de valors (freqüència) per cada atribut.
for categoric_variable in categorical_columns:
print(df[categoric_variable].value_counts())
print("suma de valors = ",df[categoric_variable].value_counts().sum())
print("\n")
job blue-collar 9732 management 9458 technician 7597 admin. 5171 services 4154 retired 2264 self-employed 1579 entrepreneur 1487 unemployed 1303 housemaid 1240 student 938 Name: count, dtype: int64 suma de valors = 44923 marital married 27214 single 12790 divorced 5207 Name: count, dtype: int64 suma de valors = 45211 education secondary 23202 tertiary 13301 primary 6851 Name: count, dtype: int64 suma de valors = 43354 default no 44396 yes 815 Name: count, dtype: int64 suma de valors = 45211 housing yes 25130 no 20081 Name: count, dtype: int64 suma de valors = 45211 loan no 37967 yes 7244 Name: count, dtype: int64 suma de valors = 45211 contact cellular 29285 telephone 2906 Name: count, dtype: int64 suma de valors = 32191 month may 13766 jul 6895 aug 6247 jun 5341 nov 3970 apr 2932 feb 2649 jan 1403 oct 738 sep 579 mar 477 dec 214 Name: count, dtype: int64 suma de valors = 45211 poutcome failure 4901 other 1840 success 1511 Name: count, dtype: int64 suma de valors = 8252 y no 39922 yes 5289 Name: count, dtype: int64 suma de valors = 45211
Seguidament, presentarem els gràfics de barres de cada variable categòrica.
#recorrem el array de categorical_columns i fem el gràfic de barres corresponent
for column in categorical_columns:
plt.figure(figsize=(10, 5))
sns.countplot(x=df[column])
plt.title(f'Gràfic de barres de {column}')
plt.xlabel(column)
plt.ylabel('Frequència')
plt.xticks(rotation=45)
plt.show()
Gràcies als gràfics, podem representar i visualitzar les dades de forma més senzilla i entenedora.
Variables numèriques¶
Realitzarem el mateix procès que abans, però aquest cop amb variables numèriques de tipus "int64".
Primer calcularem les freqüències, i després mostrarem aquestes en histogrames.
#seleccionem les variables numèriques del dataframe
numeric_columns = df.select_dtypes(include=['int64']).columns
for numeric_variable in numeric_columns:
print(df[numeric_variable].value_counts())
print("suma de valors = ",df[numeric_variable].value_counts().sum())
print("\n")
age
32 2085
31 1996
33 1972
34 1930
35 1894
...
95 2
93 2
92 2
88 2
94 1
Name: count, Length: 77, dtype: int64
suma de valors = 45211
balance
0 3514
1 195
2 156
4 139
3 134
...
14204 1
8205 1
9710 1
7038 1
4416 1
Name: count, Length: 7168, dtype: int64
suma de valors = 45211
day_of_week
20 2752
18 2308
21 2026
17 1939
6 1932
5 1910
14 1848
8 1842
28 1830
7 1817
19 1757
29 1745
15 1703
12 1603
13 1585
30 1566
9 1561
11 1479
4 1445
16 1415
2 1293
27 1121
3 1079
26 1035
23 939
22 905
25 840
31 643
10 524
24 447
1 322
Name: count, dtype: int64
suma de valors = 45211
duration
124 188
90 184
89 177
104 175
114 175
...
1286 1
1380 1
1723 1
2184 1
1233 1
Name: count, Length: 1573, dtype: int64
suma de valors = 45211
campaign
1 17544
2 12505
3 5521
4 3522
5 1764
6 1291
7 735
8 540
9 327
10 266
11 201
12 155
13 133
14 93
15 84
16 79
17 69
18 51
19 44
20 43
21 35
22 23
23 22
25 22
24 20
29 16
28 16
26 13
31 12
27 10
32 9
30 8
33 6
34 5
36 4
35 4
38 3
43 3
41 2
50 2
37 2
55 1
51 1
63 1
46 1
58 1
39 1
44 1
Name: count, dtype: int64
suma de valors = 45211
pdays
-1 36954
182 167
92 147
183 126
91 126
...
749 1
769 1
587 1
778 1
854 1
Name: count, Length: 559, dtype: int64
suma de valors = 45211
previous
0 36954
1 2772
2 2106
3 1142
4 714
5 459
6 277
7 205
8 129
9 92
10 67
11 65
12 44
13 38
15 20
14 19
17 15
16 13
19 11
23 8
20 8
18 6
22 6
24 5
27 5
29 4
21 4
25 4
30 3
26 2
37 2
28 2
38 2
51 1
275 1
58 1
32 1
40 1
55 1
35 1
41 1
Name: count, dtype: int64
suma de valors = 45211
A continuació mostrem els valors estadístics més importants de cada variable numèrica.
df[numeric_columns].describe()
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| count | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 |
| mean | 40.936210 | 1362.272058 | 15.806419 | 258.163080 | 2.763841 | 40.197828 | 0.580323 |
| std | 10.618762 | 3044.765829 | 8.322476 | 257.527812 | 3.098021 | 100.128746 | 2.303441 |
| min | 18.000000 | -8019.000000 | 1.000000 | 0.000000 | 1.000000 | -1.000000 | 0.000000 |
| 25% | 33.000000 | 72.000000 | 8.000000 | 103.000000 | 1.000000 | -1.000000 | 0.000000 |
| 50% | 39.000000 | 448.000000 | 16.000000 | 180.000000 | 2.000000 | -1.000000 | 0.000000 |
| 75% | 48.000000 | 1428.000000 | 21.000000 | 319.000000 | 3.000000 | -1.000000 | 0.000000 |
| max | 95.000000 | 102127.000000 | 31.000000 | 4918.000000 | 63.000000 | 871.000000 | 275.000000 |
#recorrem el array de numeric_columns i fem el gràfic de barres corresponent
for column in numeric_columns:
plt.figure(figsize=(10, 5))
sns.histplot(x=df[column],kde=True,bins=50)
plt.title(f'Gràfic de barres de {column}')
plt.xlabel(column)
plt.ylabel('Frequència')
plt.xticks(rotation=45)
plt.show()
Per començar, tenim la variable numèrica day_of_week que sota el meu punt de vista, s'hauria de tractar com a variable categòrica, doncs aquesta no hauria de tenir diferent valor segons el dia que és, és a dir, el diumenge dia 15, no ha de tenir diferència sobre el diumenge dia 22, i aquest últim no ha de ser més important ni representatiu que el primer pel fet de tenir un valor major.
Tractant sobre les freqüències, podem observar el següent de cada tipus de variable:
Categòriques:
- Podem observar que en aquest grup de variables, tenim quatre que contenen valors nuls: Job, Education, Contact i Poutcome. D'aquestes variables, si bé es cert que tant amb job i education podriem intentar crear el valor unknown ( doncs no falten moltes dades), amb les altres dues s'hauria de decidir que fer ja que falta més del 25% de les dades.
- Els aspectes que destacraia son que el mes en que es fan més trucades es el maig, que les persones de feina "blue-collar", "management" i "technician" suposen més de la meitat de les consultes afectuades.
- En general la majoria de trucades son a persones casades, i amb estudis secundaris o terciaris.
- Clarament, han trucat a gent que no tenia altres crèdits en mora.
- Finalment, ens centrem en que la variable objectiu demostra que no s'ha aconseguit mitjançant la campany de marqueting que la gent es subscrigui al dipòsit a termini.
Numèriques:
- Podem veure com la tendència de les edats de la gent a la que han trucat, principalment es tracta de gent joveentre els 30 i 40 anys.
- Es truca a la gent durant la part central de cada mes.
- La variable duration ens mostra com en general no s'ha aconseguit que les trucades durn massa més de 3 minuts.
- La gran majoria de persones, era la primera vegada que les trucaven.
- Durant la campana, podem veure que majoritàriament, s'han trucat a les persones una vegada; és a dir que si la gent no acceptava el dpòsit a termini, no insistien de nou.
Anàlisi exploratòria de les dades¶
En aquest subapartat explorarem gràficament la relació dels atributs descriptius amb la variable objectiu i analitzarem les diferents correlacions.
La finalitat és observar com es distribueix cadascun dels atributs en funció de la classe que tenen, per poder identificar de manera visual i ràpida si alguns atributs ens permeten predir millor que altres el valor de la variable objectiu.
Suggeriment: podeu utilitzar el paràmetre "alpha" en els gràfics perquè es puguin apreciar els dos histogrames.
for column in categorical_columns[:-1]:
fig, axes = plt.subplots(1, 2, figsize=(20, 5))
# Obtenim l'ordre de l'eix de les "X"
freq_order = df[column].value_counts().index
# gràfic de freqüència
sns.countplot(x=df[column], hue=df["y"], ax=axes[0], order=freq_order)
axes[0].set_title(f'Gràfic de barres de freqüències de {column}')
axes[0].set_xlabel(column)
axes[0].set_ylabel('Frequència')
axes[0].tick_params(axis='x', rotation=45)
# Calculem els percentatges
counts = df.groupby([column, 'y'], observed=True).size().unstack(fill_value=0)
percentages = counts.apply(lambda x: 100 * x / x.sum(), axis=1)
# Reorganitzem les dades
percentages = percentages.stack().reset_index().rename(columns={0: 'percentatge'})
# grafiquem les dades segons el seu percentatge
sns.barplot(x=percentages[column], y=percentages['percentatge'], hue=percentages['y'], ax=axes[1], order=freq_order)
axes[1].set_title(f'Gràfic de barres en percentatges de {column}')
axes[1].set_xlabel(column)
axes[1].set_ylabel('Percentatge(%)')
axes[1].tick_params(axis='x', rotation=45)
plt.show()
df_numeric = df.copy()
for column in numeric_columns:
fig, axes = plt.subplots(1, 2, figsize=(20, 5))
# Dividim la variable en intervals (bins) utilitzant .loc per evitar l'error
df_numeric.loc[:, f'{column}_bins'] = pd.cut(df_numeric[column], bins=30)
# Mantenir "y" com a última columna
columns = [col for col in df_numeric.columns if col != 'y'] + ['y']
df_numeric = df_numeric[columns]
# Obtenim l'ordre de l'eix X basat en les freqüències
bin_order = df_numeric[f'{column}_bins'].value_counts().index
# Gràfic de barres amb les freqüències per intervals
sns.countplot(x=df_numeric[f'{column}_bins'], hue=df_numeric["y"], ax=axes[0], order=bin_order)
axes[0].set_title(f'Gràfic de barres de freqüències (per intervals) de {column}')
axes[0].set_xlabel(f'{column} (intervals)')
axes[0].set_ylabel('Frequència')
axes[0].tick_params(axis='x', rotation=45)
counts = df_numeric.groupby([f'{column}_bins', 'y'], observed=True).size().unstack(fill_value=0)
percentages = counts.apply(lambda x: 100 * x / x.sum(), axis=1)
# Reorganitzem les dades
percentages = percentages.stack().reset_index().rename(columns={0: 'percentatge'})
# Gràfic de barres amb percentatges per intervals
sns.barplot(x=percentages[f'{column}_bins'], y=percentages['percentatge'], hue=percentages['y'], ax=axes[1], order=bin_order)
axes[1].set_title(f'Gràfic de barres en percentatges (per intervals) de {column}')
axes[1].set_xlabel(f'{column} (intervals)')
axes[1].set_ylabel('Percentatge (%)')
axes[1].tick_params(axis='x', rotation=45)
plt.show()
df_numeric.describe()
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| count | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 | 45211.000000 |
| mean | 40.936210 | 1362.272058 | 15.806419 | 258.163080 | 2.763841 | 40.197828 | 0.580323 |
| std | 10.618762 | 3044.765829 | 8.322476 | 257.527812 | 3.098021 | 100.128746 | 2.303441 |
| min | 18.000000 | -8019.000000 | 1.000000 | 0.000000 | 1.000000 | -1.000000 | 0.000000 |
| 25% | 33.000000 | 72.000000 | 8.000000 | 103.000000 | 1.000000 | -1.000000 | 0.000000 |
| 50% | 39.000000 | 448.000000 | 16.000000 | 180.000000 | 2.000000 | -1.000000 | 0.000000 |
| 75% | 48.000000 | 1428.000000 | 21.000000 | 319.000000 | 3.000000 | -1.000000 | 0.000000 |
| max | 95.000000 | 102127.000000 | 31.000000 | 4918.000000 | 63.000000 | 871.000000 | 275.000000 |
Mirant els gràfics de barres i els histogrames, quins atributs semblen tenir més pes al moment de predir si es contractarà un dipòsit a termini o no? Creus que amb aquests atributs serà suficient per poder determinar si es contractarà o no el dipòsit?
Fent un analisi dels gràfics de barres, histogrames i les dades estadístiques, podem observar el següent:
Variables numèriques:
- En la variable duration, es mostra una clara diferència en la distribució entre els clients que contracten el dipòsit i els que no. Les trucades més llargues semblen estar associades amb una major probabilitat de contractació. És un indicador important perquè pot reflectir un major interès o disponibilitat per part del client.
- Amb la variable campaign podem veure com de persistent ha estat el banc amb un mateix client, això pot indicar que si son mes persistents, hi ha més opcions de convencer al client.No obstant això, l'impacte és menor en comparació amb duration.
- La variable pdays pot ser rellevant per determinar l'efectivitat de la campanya i el moment òptim per recontactar.
- Tot i que la major part dels clients es concentren en balanços (balance) baixos, hi ha una petita proporció de clients amb un balanç més alt que també mostren un major percentatge de contractació del dipòsit. Això podria ser rellevant, ja que els clients amb més recursos poden tenir més probabilitats de contractar productes financers.
Variables categòriques:
- A la variable job es pot observar que certes ocupacions com management, retired i student tenen una proporció més alta de clients que contracten el dipòsit en comparació amb ocupacions com blue-collar. Això suggereix que el tipus de feina pot ser un factor rellevant, ja que indica el perfil socioeconòmic del client.
- Els clients amb educació (education) terciària tenen una proporció més alta de contractació del dipòsit, mentre que els amb educació secundària o primària tenen una taxa de conversió menor. El nivell educatiu pot ser un bon predictor, ja que està relacionat amb la capacitat d'estalvi i la disposició a contractar productes financers.
- Certs mesos (month), com may, mostren un volum molt alt de trucades però una baixa taxa de conversió. D'altra banda, mesos com oct o dec tenen menys trucades però una proporció relativament més alta de clients que contracten. El mes en què es fa la trucada pot influir en la probabilitat d’èxit, possiblement per factors estacionals o de campanya.
- Els clients amb un success en una campanya anterior (poutcome) tenen una taxa de conversió molt més alta que altres categories. Això indica que l'historial d'interacció amb el client pot ser un factor molt determinant.
- A la variable contact observem com les trucades a través del cellular semblen tenir una major taxa d’èxit en comparació amb les trucades telefòniques tradicionals (telephone). Aquest atribut podria ser important per identificar quin canal de contacte és més efectiu.
Si bé es cert que amb les dades que tenim, podem cobrir areas com Demografia, economia, context (mesos)... i això ens permetria construir un model predictiu força eficaç, podrien faltar variables com els salaris mensuals, el marge d'estalvi entre d'altres. Per altre banda, si bé es cert que tenim moltes dades, aquestes estan esbiaixades doncs hi ha molt més percentatge de dades on la variable objectiu es "no", que no pas "sí", i això podria fer que qualsevol model predictiu aprenguès malament i per conseqüent, no predigui correctament si es contractarà o no el dipòsit. Així doncs, sí que és suficient per començar, a fer coses però s'ha d'arreglar el problema de l 'esbiaix de les dades'.
en el moment de correlacionar dades, em sembla molt interessant mostrar primer tots els gràfics enfrentats i després fer la matriu de correlació.
g = sns.pairplot(data=df,
corner=True,
hue="y")
g.fig.suptitle("Relació entre variables numèriques del dataset",
va="baseline",
ha="center",
fontsize=16)
Text(0.5, 0.98, 'Relació entre variables numèriques del dataset')
# Calculem la matriu de correlació
df['y_binary'] = df['y'].map({'yes': 1, 'no': 0})
numeric_columns_with_target = df.select_dtypes(include=['int64']).columns
correlation_matrix = df[numeric_columns_with_target].corr()
# Visualitzem la matriu de correlació amb un heatmap
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title('Correlation Matrix of Numerical Variables')
plt.show()
Podem veure com duration és clau doncs te una correlació de 0.39, això significa que com més llarga és la durada de la trucada, més probable és que el client acabi contractant el dipòsit. Aquesta és una observació lògica, ja que els clients que mostren interès tendeixen a mantenir trucades més llargues.
La variable pdays (dies des de l'últim contacte) també mostra una lleugera correlació positiva la variable objectiu. Això indica que el temps transcorregut des de l'últim contacte pot influir en la probabilitat que el client contracti el dipòsit, tot i que aquesta influència és moderada.
No obstant això, podem veure que les correlacions son en general molt baixes. Caldria observar si modificant el dataset (eliminant el esbiaix de dades) podriem treure alguna cosa més en clar.
Preprocessament de les dades (1.5 punts)¶
Un cop analitzats els atributs descriptius, és el moment de preparar-los perquè ens siguin útils amb vista a predir valors.
A partir d’aquest punt, per simplicitat, treballarem únicament amb els atributs numèrics.
En aquest apartat:
Hi ha moltes tècniques que es poden utilitzar per codificar variables categòriques, com ara:
- Label Encoding
- One-Hot Encoding
- Target Encoding
- Mean Encoding
- Frequency Encoding
Cada tècnica és més eficient segons el tipus de tasca. En el nostre cas, la millor opció seria utilitzar One-Hot Encoding, ja que els valors representen categories sense cap ordre implícit.
Quan diem que les categories no tenen un ordre, ens referim a casos com el de la columna marital amb els valors "married", "single" i "divorced". Si utilitzéssim Label Encoding per codificar aquestes categories, es podrien assignar valors com 1, 2 i 3. Això faria que "divorced" tingués un valor més alt que "married" o "single", cosa que podria induir el model a pensar que hi ha una jerarquia entre aquestes categories, creant biaixos en les prediccions.
Amb One-Hot Encoding, evitem aquest problema perquè creem noves columnes per a cada categoria (en aquest cas, "married", "single" i "divorced") i assignem valors binaris (0 o 1) segons la presència o absència d'aquesta categoria en cada registre. D'aquesta manera, no hi ha cap ordre o relació numèrica implícita entre les categories, eliminant possibles biaixos.
Suggeriment: utilitzeu "StandardScaler" de preprocessing.
from sklearn.preprocessing import StandardScaler
numdf = df[numeric_columns]
scaler = StandardScaler()
norm_data = scaler.fit_transform(numdf)
normdf = pd.DataFrame(norm_data,columns=numdf.columns)
normdf.head(10)
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| 0 | 1.606965 | 0.256419 | -1.298476 | 0.011016 | -0.569351 | -0.411453 | -0.25194 |
| 1 | 0.288529 | -0.437895 | -1.298476 | -0.416127 | -0.569351 | -0.411453 | -0.25194 |
| 2 | -0.747384 | -0.446762 | -1.298476 | -0.707361 | -0.569351 | -0.411453 | -0.25194 |
| 3 | 0.571051 | 0.047205 | -1.298476 | -0.645231 | -0.569351 | -0.411453 | -0.25194 |
| 4 | -0.747384 | -0.447091 | -1.298476 | -0.233620 | -0.569351 | -0.411453 | -0.25194 |
| 5 | -0.559037 | -0.371551 | -1.298476 | -0.462724 | -0.569351 | -0.411453 | -0.25194 |
| 6 | -1.218254 | -0.300608 | -1.298476 | -0.159841 | -0.569351 | -0.411453 | -0.25194 |
| 7 | 0.100181 | -0.446762 | -1.298476 | 0.473107 | -0.569351 | -0.411453 | -0.25194 |
| 8 | 1.606965 | -0.407679 | -1.298476 | -0.808322 | -0.569351 | -0.411453 | -0.25194 |
| 9 | 0.194355 | -0.252657 | -1.298476 | -0.788906 | -0.569351 | -0.411453 | -0.25194 |
normdf.describe()
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| count | 4.521100e+04 | 4.521100e+04 | 4.521100e+04 | 4.521100e+04 | 4.521100e+04 | 4.521100e+04 | 4.521100e+04 |
| mean | 2.112250e-16 | 1.760208e-17 | 1.257292e-17 | 6.035001e-17 | 3.017500e-17 | 2.011667e-17 | 4.023334e-17 |
| std | 1.000011e+00 | 1.000011e+00 | 1.000011e+00 | 1.000011e+00 | 1.000011e+00 | 1.000011e+00 | 1.000011e+00 |
| min | -2.159994e+00 | -3.081149e+00 | -1.779108e+00 | -1.002478e+00 | -5.693506e-01 | -4.114531e-01 | -2.519404e-01 |
| 25% | -7.473845e-01 | -4.237719e-01 | -9.380027e-01 | -6.025167e-01 | -5.693506e-01 | -4.114531e-01 | -2.519404e-01 |
| 50% | -1.823406e-01 | -3.002800e-01 | 2.326031e-02 | -3.035165e-01 | -2.465603e-01 | -4.114531e-01 | -2.519404e-01 |
| 75% | 6.652252e-01 | 2.158743e-02 | 6.240497e-01 | 2.362370e-01 | 7.622994e-02 | -4.114531e-01 | -2.519404e-01 |
| max | 5.091402e+00 | 3.309478e+01 | 1.825628e+00 | 1.809470e+01 | 1.944365e+01 | 8.297431e+00 | 1.191360e+02 |
Suggeriment: per separar entre entrenament i prova podeu utilitzar "train_test_split" de sklearn.
El primer que farem serà unir el nostre dataframe "normdf" amb la variable independent "y"
normdf["y"] = df["y"]
normdf.head(10)
| age | balance | day_of_week | duration | campaign | pdays | previous | y | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1.606965 | 0.256419 | -1.298476 | 0.011016 | -0.569351 | -0.411453 | -0.25194 | no |
| 1 | 0.288529 | -0.437895 | -1.298476 | -0.416127 | -0.569351 | -0.411453 | -0.25194 | no |
| 2 | -0.747384 | -0.446762 | -1.298476 | -0.707361 | -0.569351 | -0.411453 | -0.25194 | no |
| 3 | 0.571051 | 0.047205 | -1.298476 | -0.645231 | -0.569351 | -0.411453 | -0.25194 | no |
| 4 | -0.747384 | -0.447091 | -1.298476 | -0.233620 | -0.569351 | -0.411453 | -0.25194 | no |
| 5 | -0.559037 | -0.371551 | -1.298476 | -0.462724 | -0.569351 | -0.411453 | -0.25194 | no |
| 6 | -1.218254 | -0.300608 | -1.298476 | -0.159841 | -0.569351 | -0.411453 | -0.25194 | no |
| 7 | 0.100181 | -0.446762 | -1.298476 | 0.473107 | -0.569351 | -0.411453 | -0.25194 | no |
| 8 | 1.606965 | -0.407679 | -1.298476 | -0.808322 | -0.569351 | -0.411453 | -0.25194 | no |
| 9 | 0.194355 | -0.252657 | -1.298476 | -0.788906 | -0.569351 | -0.411453 | -0.25194 | no |
A continuació dividirem en train i en test el nostre dataframe.
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(normdf.iloc[:,:-1],normdf["y"], test_size=0.3, random_state=42)
print("X train: ")
print(X_train.head())
print("\n X test")
print(X_test.head())
print("\nY train: ")
print(y_train.head())
print("\n Y test")
print(y_test.head())
X train:
age balance day_of_week duration campaign pdays previous
10747 -0.464863 -0.447419 0.143418 -0.408361 0.399020 -0.411453 -0.251940
26054 1.418617 -0.383046 0.383734 0.209055 0.076230 -0.411453 -0.251940
9125 0.476877 -0.447419 -1.298476 -0.680179 -0.246560 -0.411453 -0.251940
41659 0.006007 0.677803 -1.779108 0.170224 -0.569351 0.787017 1.918749
4443 -0.276515 -0.447419 0.503892 -0.652997 -0.569351 -0.411453 -0.251940
X test
age balance day_of_week duration campaign pdays previous
3776 -0.088167 -0.256926 0.023260 -0.256919 -0.569351 -0.411453 -0.251940
9928 0.571051 0.749402 -0.817845 -0.680179 -0.246560 -0.411453 -0.251940
33409 -1.500776 -0.270721 0.503892 -0.124893 -0.569351 -0.411453 -0.251940
31885 0.100181 0.134898 -0.817845 0.205172 -0.569351 2.954251 0.182198
15738 1.418617 -0.376149 0.624050 -0.532621 -0.246560 -0.411453 -0.251940
Y train:
10747 no
26054 no
9125 no
41659 no
4443 no
Name: y, dtype: object
Y test
3776 no
9928 no
33409 no
31885 no
15738 no
Name: y, dtype: object
No és una bona idea estandarditzar les dades abans de separar-les en subconjunts d’entrenament i prova. Si l'estandarització es fa abans, el procés utilitza informació de tot el conjunt de dades per calcular les mitjanes i desviacions estàndard, incloent el subconjunt de prova, cosa que pot introduir biaixos. La manera correcta és separar primer les dades i després estandarditzar només el conjunt d’entrenament, aplicant els mateixos paràmetres al conjunt de prova per assegurar una avaluació justa.
Quan les variables tenen escales molt diferents, un model de machine learning pot donar més importància a les variables amb valors més grans. Per exemple, si tens una variable amb valors entre 1 i 10 (com l'edat) i una altra amb valors entre 0.01 i 10000 (com els ingressos), el model pot prestar més atenció a la segona variable simplement perquè els seus valors són més grans. L'estandardització assegura que totes les variables tinguin una mitjana de 0 i una desviació estàndard de 1, permetent que totes contribueixin de manera equilibrada al model.
Per altre banda, molts algoritmes de machine learning com la regressió logística, les xarxes neuronals o els mètodes de gradient descendent necessiten dades estandarditzades per a convergir de manera més eficient. L'estandardització ajuda a que l'entrenament sigui més ràpid i estable, ja que evita que algunes variables amb valors grans afectin el procés d'optimització.
Reducció de la dimensionalitat (2.5 punts)¶
En aquest apartat reprendrem l'anàlisi gràfica de la distribució de la classe al llarg de les mostres del conjunt de dades. En el segon apartat vam poder observar si les variables descriptives per separat eren molt prometedores o no per predir la classe. Aquí intentarem determinar si la seva combinació pot ajudar-nos a establir si es contractarà el dipòsit de manera més eficaç que utilitzant els atributs per separat. Amb aquest propòsit, reduirem la dimensionalitat del problema a només dos atributs, que seran la projecció dels atributs descriptius originals, i observarem com es distribueixen les mostres de cada classe.
- Aplica el mètode de reducció de la dimensionalitat Principal Component Analysis (PCA) per reduir a 2 dimensions el conjunt de dades complet amb tots els atributs (*features*).
- Genera un gràfic en 2D amb el resultat del PCA utilitzant colors diferents per a cadascuna de les classes de la resposta, amb l'objectiu de visualitzar si és possible separar eficientment les classes amb aquest mètode.
NOTA: Tingueu cura de no incloure la variable objectiu en la reducció de dimensionalitat. Volem explicar la variable objectiu en funció de la resta de variables reduïdes a dues dimensions.
Suggeriment: no és necessari que programeu l'algorisme de PCA, podeu utilitzar la implementació disponible a la llibreria de scikit-learn.
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(normdf.iloc[:,:-1])
pca_df = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
# Afegir la columna de classe (df['y']) per a utilitzar-la com a color
pca_df['Target'] = df['y']
# Generar el gràfic 2D amb colors diferents per a les classes
plt.figure(figsize=(10, 8))
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7)
plt.title('PCA (2 Components) amb el Target pintat (yes/no)')
plt.xlabel('Component Principal 1')
plt.ylabel('Component Principal 2')
plt.grid(True)
plt.show()
- Repeteix la reducció de la dimensionalitat, però en aquest cas utilitzant TSNE. Podeu trobar més informació sobre aquest algorisme en l'enllaç: https://distill.pub/2016/misread-tsne/
- Igual que abans, genereu un gràfic en 2D amb el resultat del TSNE fent servir colors diferents per a cadascuna de les classes de la resposta (y), amb l'objectiu de visualitzar si és possible separar eficientment les classes amb aquest mètode.
Suggeriment: no és necessari que programeu l'algorisme TSNE, podeu fer servir la implementació disponible a la llibreria de scikit-learn.
Suggeriment: a part d'especificar el nombre de components, proveu els paràmetres "learning_rate" i "perplexity".
from sklearn.manifold import TSNE
# fem la variable y binaria
df['y_binary'] = df['y'].apply(lambda x: 1 if x == 'yes' else 0)
# reduim a 2 components amb t-SNE
tsne = TSNE(n_components=2, learning_rate=100, perplexity=30, random_state=42)
X_tsne = tsne.fit_transform(normdf.iloc[:,:-1])
# Convertim a dataframe
tsne_df = pd.DataFrame(X_tsne, columns=['Component 1', 'Component 2'])
tsne_df['Target'] = df['y_binary']
# Plot
plt.figure(figsize=(10, 8))
sns.scatterplot(data=tsne_df, x='Component 1', y='Component 2', hue='Target', palette='coolwarm', alpha=0.7)
plt.title('Resultat de t-SNE (2 Components) amb el target pintat')
plt.xlabel('Component 1')
plt.ylabel('Component 2')
plt.grid(True)
plt.show()
C:\Users\tvive\anaconda3\envs\uoc20241pec1\Lib\site-packages\joblib\externals\loky\backend\context.py:136: UserWarning: Could not find the number of physical cores for the following reason:
found 0 physical cores < 1
Returning the number of logical cores instead. You can silence this warning by setting LOKY_MAX_CPU_COUNT to the number of cores you want to use.
warnings.warn(
File "C:\Users\tvive\anaconda3\envs\uoc20241pec1\Lib\site-packages\joblib\externals\loky\backend\context.py", line 282, in _count_physical_cores
raise ValueError(f"found {cpu_count_physical} physical cores < 1")
En el cas de la reducció de dimensionalitat amb PCA, podem observar que les dues classes (yes i no) es troben força superposades, la qual cosa indica que no hi ha una separació clara entre les dues. Per tant podem dir que PCA no ha funcionat de manera efectiva. D'altra banda, el gràfic obtingut amb t-SNE mostra una separació lleugerament millor. Tot i que les classes segueixen una mica barrejades, t-SNE ha aconseguit captar certes àrees on els punts es separen més clarament([(0,40),(-20,20)]), de manera que aquest mètode ens permetria separar millor en base a la variable objectiu.
Els resultats són tan diferents perquè PCA i t-SNE aborden la reducció de dimensionalitat de maneres molt diferents. PCA se centra en maximitzar la variància global de les dades, i això funciona bé quan les dimensions de major variància coincideixen amb les que separen les classes. No obstant això, si les diferències entre les classes no es troben en les dimensions de major variància, com sembla ser el cas aquí, PCA no serveix. D'altra banda, t-SNE prioritza la preservació de les distàncies locals entre punts, la qual cosa li permet capturar patrons subtils i diferenciacions entre classes que no depenen de la variància global. Per això, en aquest cas, t-SNE ha funcionat millor en la separació de les classes.
t-SNE és una opció molt potent per a la reducció de dimensionalitat quan l'objectiu és visualitzar dades complexes en espais de baixa dimensió, especialment quan es busca captar agrupacions locals. És particularment útil per a tasques de visualització, ja que pot capturar estructures i relacions entre dades que altres mètodes, com PCA, no poden reflectir. No obstant això, t-SNE també té limitacions importants: és computacionalment costós i no conserva bé les relacions globals entre punts, per la qual cosa pot distorsionar la imatge general de les dades.
El fet que t-SNE només ofereixi el mètode fit_transform però no transform és una limitació important, especialment si es vol aplicar el model de t-SNE a noves dades. Això significa que cada vegada que es vol transformar un conjunt de dades, s'ha de recalcular tota la transformació des de zero, cosa que és molt costosa en termes de temps i recursos computacionals.
Una alternativa que aborda algunes de les limitacions de t-SNE és UMAP (Uniform Manifold Approximation and Projection). UMAP ofereix avantatges similars a t-SNE pel que fa a la capacitat de capturar relacions locals i visualitzar agrupacions en espais de baixa dimensió, però amb alguns beneficis addicionals. UMAP és generalment més ràpid i escalable, i té tant un mètode fit com un mètode transform, la qual cosa el fa molt més eficient per aplicar a noves dades sense haver de recalcular tota la transformació. A més, UMAP preserva millor les relacions globals entre punts, cosa que permet visualitzacions que conserven tant la informació local com global.
Conjunts desequilibrats de dades (2.5 punts)¶
En els problemes de classificació, és molt comú trobar conjunts de dades molt desequilibrats. En la indústria existeixen múltiples exemples, com ara la detecció de frau o la fuga de clients. Aquest apartat se centra en l’anàlisi d’aquest tipus de conjunts.
El cas del dataset amb el qual estem treballant (Bank Marketing) és un d’ells, ja que podem observar com la classe "no" apareix amb una freqüència fins a deu vegades més gran que la classe "yes".
A continuació, analitzarem la distribució del nostre conjunt de dades. Per fer-ho, farem servir la funció show_distribution definida a la següent cel·la:
def show_distribution(y_df):
freq = y_df["y"].value_counts()
plt.pie(freq, labels=('No subscription ('+str(freq["no"])+')', 'Subscription ('+str(freq["yes"])+')'), autopct='%1.1f%%')
plt.title("Term deposit subscription distribution")
plt.show()
show_distribution(y)
Com es pot observar, el conjunt està força desequilibrat, ja que, pràcticament, només una desena part de les mostres corresponen a la contractació del dipòsit.
Per tractar el problema de dades desequilibrades, analitzarem la tècnica de sobremostreig (oversampling) de la classe minoritària. A la literatura hi ha més tècniques per afrontar aquest problema, com el submostreig (undersampling) de la classe majoritària, però en aquesta PAC, ens centrarem només en la tècnica de sobremostreig.
Oversampling¶
- Duplicació aleatòria (_random over-sampling_), fixant random_state=10.
- SMOTE (_Synthetic Minority Over-sampling Technique_), fixant random_state=10.
- ADASYN (_Adaptive Synthetic Sampling_), fixant random_state=10.
Per acabar, verifiqueu amb l’ajuda de la funció show_distribution, que després de l’aplicació d’aquestes tècniques, el nombre de mostres de la classe minoritària s’ha igualat al de la majoritària.
Suggeriment: per aplicar la replicació aleatòria podeu fer servir "RandomOverSampler" d'imblearn.
Suggeriment: per aplicar SMOTE podeu utilitzar "SMOTE" d'imblearn.
Suggeriment: per aplicar ADASYN podeu utilitzar "ADASYN" d'imblearn.
from imblearn.over_sampling import RandomOverSampler
numbers_df = df[numeric_columns]
numbers_df
# Creant un objecte RandomOverSampler
ros = RandomOverSampler(random_state=10)
# Aplicant Random Over-Sampling
X_resampled_ros, y_resampled_ros = ros.fit_resample(numbers_df, y)
show_distribution(y_resampled_ros)
y_resampled_ros.describe()
| y | |
|---|---|
| count | 79844 |
| unique | 2 |
| top | no |
| freq | 39922 |
from imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=10)
X_res_smote, y_res_smote = sm.fit_resample(numbers_df, y)
show_distribution(y_res_smote)
y_res_smote.describe()
| y | |
|---|---|
| count | 79844 |
| unique | 2 |
| top | no |
| freq | 39922 |
from imblearn.over_sampling import ADASYN
ada = ADASYN(random_state=10)
X_res_adasyn, y_res_adasyn = ada.fit_resample(numbers_df, y)
show_distribution(y_res_adasyn)
y_res_adasyn.head()
| y | |
|---|---|
| 0 | no |
| 1 | no |
| 2 | no |
| 3 | no |
| 4 | no |
El resultat d'aplicar aquestes tècniques hauria d'haver produit un nombre similar de mostres per a les dues classes. No obstant això, cadascun dels mètodes genera les noves mostres de la classe minoritària de manera diferent. Amb l'objectiu de comprendre millor i de manera visual com es generen aquestes noves mostres, a partir d'ara, farem servir la descomposició a dues dimensions que hagi mostrat un millor comportament a l'apartat anterior.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
norm_data_oversampled = scaler.fit_transform(X_resampled_ros)
normdf_oversampled = pd.DataFrame(norm_data_oversampled,columns=numdf.columns)
normdf_oversampled.head(10)
| age | balance | day_of_week | duration | campaign | pdays | previous | |
|---|---|---|---|---|---|---|---|
| 0 | 1.404682 | 0.187404 | -1.253967 | -0.333576 | -0.55888 | -0.489594 | -0.341155 |
| 1 | 0.231035 | -0.472799 | -1.253967 | -0.647103 | -0.55888 | -0.489594 | -0.341155 |
| 2 | -0.691117 | -0.481231 | -1.253967 | -0.860871 | -0.55888 | -0.489594 | -0.341155 |
| 3 | 0.482531 | -0.011531 | -1.253967 | -0.815267 | -0.55888 | -0.489594 | -0.341155 |
| 4 | -0.691117 | -0.481544 | -1.253967 | -0.513141 | -0.55888 | -0.489594 | -0.341155 |
| 5 | -0.523453 | -0.409715 | -1.253967 | -0.681306 | -0.55888 | -0.489594 | -0.341155 |
| 6 | -1.110277 | -0.342258 | -1.253967 | -0.458987 | -0.55888 | -0.489594 | -0.341155 |
| 7 | 0.063371 | -0.481231 | -1.253967 | 0.005602 | -0.55888 | -0.489594 | -0.341155 |
| 8 | 1.404682 | -0.444068 | -1.253967 | -0.934977 | -0.55888 | -0.489594 | -0.341155 |
| 9 | 0.147203 | -0.296662 | -1.253967 | -0.920726 | -0.55888 | -0.489594 | -0.341155 |
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(normdf_oversampled)
pca_df_ros = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
# afegm "y" per posar els colors
pca_df_ros['Target'] = y_resampled_ros
plt.figure(figsize=(10, 8))
sns.scatterplot(data=pca_df_ros, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7)
plt.title('PCA (2 Components) amb Target (yes/no) i oversampling')
plt.xlabel('Component Principal 1')
plt.ylabel('Component Principal 2')
plt.grid(True)
plt.show()
from sklearn.manifold import TSNE
# convertim 'y' a binari
df_TSNE = pd.DataFrame(y_resampled_ros, columns=['y'])
df_TSNE['y_binary'] = df_TSNE['y'].apply(lambda x: 1 if x == 'yes' else 0)
tsne = TSNE(n_components=2, learning_rate=100, perplexity=30, random_state=10)
X_tsne = tsne.fit_transform(X_resampled_ros)
tsne_df = pd.DataFrame(X_tsne, columns=['Component 1', 'Component 2'])
tsne_df['Target'] = df_TSNE['y_binary']
plt.figure(figsize=(10, 8))
sns.scatterplot(data=tsne_df, x='Component 1', y='Component 2', hue='Target', palette='coolwarm', alpha=0.7)
plt.title('t-SNE (2 Components) amb Target (1/0) i oversampling')
plt.xlabel('Component 1')
plt.ylabel('Component 2')
plt.grid(True)
plt.show()
Ara, les classes yes i no tenen una representació més equitativa, el que fa que les dues classes tinguin una presència visual similar en l'espai reduït a 2 dimensions. Això permet que les tècniques de visualització mostrin una separació més clara i una distribució més homogènia de les classes, la qual cosa no era possible abans, amb la distribució desbalancejada. Gràcies a equiparar les dades en la columna de la variable objectiu, podem obtenir uns gràfics que ens permeten diferenciar les dues classes amb més facilitat, doncs en els dos casos podem observar que les opcions de "yes" s'agrupen i les de "no" també.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
numbers_df = df[numeric_columns]
# Normalitzem les dades
scaler = StandardScaler()
numbers_df_scaled = scaler.fit_transform(numbers_df)
# RandomOverSampler
ros = RandomOverSampler(random_state=10)
X_resampled_ros, y_resampled_ros = ros.fit_resample(numbers_df_scaled, y)
# SMOTE
sm = SMOTE(random_state=10)
X_res_smote, y_res_smote = sm.fit_resample(numbers_df_scaled, y)
# ADASYN
ada = ADASYN(random_state=10)
X_res_adasyn, y_res_adasyn = ada.fit_resample(numbers_df_scaled, y)
# Funció per aplicar PCA
def pca_dataframe(X, y):
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
pca_df = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
pca_df['Target'] = y
return pca_df
# PCA per a cada tècnica
pca_df_ros = pca_dataframe(X_resampled_ros, y_resampled_ros)
pca_df_smote = pca_dataframe(X_res_smote, y_res_smote)
pca_df_adasyn = pca_dataframe(X_res_adasyn, y_res_adasyn)
# Crear la figura i els subgràfics
fig, axs = plt.subplots(1, 3, figsize=(18, 6), sharex=True, sharey=True)
# Graficar
sns.scatterplot(data=pca_df_ros, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[0])
axs[0].set_title('RandomOverSampler')
axs[0].set_xlabel('PC1')
axs[0].set_ylabel('PC2')
sns.scatterplot(data=pca_df_smote, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[1])
axs[1].set_title('SMOTE')
axs[1].set_xlabel('PC1')
axs[1].set_ylabel('')
sns.scatterplot(data=pca_df_adasyn, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[2])
axs[2].set_title('ADASYN')
axs[2].set_xlabel('PC1')
axs[2].set_ylabel('')
# Ajustar espais i mostrar el gràfic
plt.suptitle('Distribució PCA (2 Components) per a cada tècnica d\'oversampling')
plt.tight_layout()
plt.show()
Random Over-Sampling simplement duplica mostres existents de manera aleatòria. No afegeix noves dades sintètiques, de manera que la distribució de punts no varia respecte a l'original; només augmenta la seva quantitat per igualar la classe majoritària.
SMOTE genera noves mostres fent una interpolació entre els veïns més propers, fet que provoca una distribució més fluida i contínua de les dades. Aquesta tècnica és efectiva per evitar el sobreajustament i afegir variabilitat sintètica.
ADASYN ajusta la quantitat de mostres generades segons la distribució de les dades, amb un enfocament adaptatiu que afavoreix les àrees més crítiques. Això permet que la classe minoritària es distribueixi millor en les àrees on es troben menys mostres, equilibrant més eficientment les classes.
Tant SMOTE com ADASYN creen mostres sintètiques, però mentre SMOTE genera mostres entre els punts existents de manera més homogènia, ADASYN se centra més en les àrees menys denses, creant una distribució més equilibrada i adaptativa.